package com.yifei.utils;

import cn.hutool.core.convert.Convert;
import com.yifei.constants.RedisKeyConstants;
import com.yifei.model.dto.auth.LoginResult;
import com.yifei.model.dto.auth.UserAuthInfo;
import com.yifei.model.entity.auth.SysUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Description:
 * @author: yiFei
 * @date: 2022/1/3 23:04
 */
@Component
public class JwtUtil {

    private JwtUtil() {
    }

    /**
     * 生成 token 的前缀
     */
    @Value("${jwt.token-header}")
    private String jwtTokenHeader;
    /**
     * 请求头存储 token 的 key
     */
    @Value("${jwt.request-header-key}")
    private String JwtRequestHeaderKey;
    /**
     * JWT 密匙
     */
    @Value("${jwt.secret-key}")
    private String JwtSecretKey;
    /**
     * JWT 过期时间（小时为单位）
     */
    @Value("${jwt.expiration-hours}")
    private Integer JwtExpirationHours;
    /**
     * JWT 过期时间（小时为单位）
     */
    @Value("${jwt.refresh-expiration-hours}")
    private Integer JwtRefreshExpirationHours;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 创建一个 JwtToken
     *
     * @param authentication 权限对象
     * @return @Link(LoginResult)
     */
    public LoginResult createJwtToken(Authentication authentication) {
        Object principal = authentication.getPrincipal();
        // 对 principal 进行校验，为空直接返回
        if (ObjectUtils.isEmpty(principal)) {
            return null;
        }
        // 如果获取到的对象不是 SysUserDetails 直接返回
        if (!(principal instanceof SysUserDetails)) {
            return null;
        }
        SysUserDetails sysUserDetails = (SysUserDetails) principal;
        return getLoginResultInfo(sysUserDetails);
    }

    /**
     * 返回存入 SpringSecurity ContextHolder 的对象
     *
     * @param token 用户 token
     * @return Authentication
     */
    public Authentication getAuthentication(String token) {
        Claims claims = this.getClaim(token);
        if (ObjectUtils.isEmpty(claims)) {
            return null;
        }
        // 这里使用构造方法设置角色，会导致多次增加ROLE_ , 生成的权限为 ROLE_ROLE_?
        SysUserDetails userDetails = new SysUserDetails(
                UserAuthInfo.builder()
                        .userId(Convert.toLong(claims.get("userId")))
                        .username(Convert.toStr(claims.get("username")))
                        .build()
        );
        // 构建权限信息 写入 UsernamePasswordAuthenticationToken 通过 Security 给全局使用
        Set<String> roles = Convert.toSet(String.class, claims.get("roles"));
        Set<SimpleGrantedAuthority> authorities = roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet());
        // 返回 UsernamePasswordAuthenticationToken
        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
    }

    /**
     * @param token
     * @param refreshToken
     * @return 返回用户名
     */
    public String getRefreshTokenUsername(String token, String refreshToken) {
        Claims claim = getClaim(cutToken(refreshToken));
        if (ObjectUtils.isEmpty(claim)) {
            return null;
        }
        String claimToken = Convert.toStr(claim.get("token"));
        if (StringUtils.hasText(claimToken) && claimToken.equals(cutToken(token))) {
            return Convert.toStr(claim.get("username"));
        } else {
            return null;
        }
    }

    /**
     * 通过 sysUserDetails 构建 LoginResult （ 其中包含填充 LoginResult ）
     *
     * @param sysUserDetails
     * @return
     */
    public LoginResult getLoginResultInfo(SysUserDetails sysUserDetails) {
        // 构建角色信息
        Set<String> roles = sysUserDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
        // 获取 Jwt 过期时间
        Date jwtExpirationDate = getExpirationTime(JwtExpirationHours);

        // 生成 token 信息
        String token = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, JwtSecretKey)
                //  将信息放入 claim
                .claim("userId", sysUserDetails.getUserId())
                .claim("username", sysUserDetails.getUsername())
                .claim("roles", roles)
                // 设置过期时间
                .setExpiration(jwtExpirationDate)
                .compact();

        // 生成 refreshToken 信息
        String refreshToken = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, JwtSecretKey)
                //  将信息放入 claim
                .claim("username", sysUserDetails.getUsername())
                .claim("token", token)
                // 设置过期时间
                .setExpiration(getExpirationTime(JwtRefreshExpirationHours))
                .compact();

        // 将 权限按钮信息写入 Redis 方便以后使用 ( 通过Redis存储的数据，可以查询多少用户登录，以及消除用户权限 )
        redisUtil.setCacheObject(RedisKeyConstants.USER_PERMS_CACHE_PREFIX + sysUserDetails.getUserId(), sysUserDetails.getPerms(), JwtRefreshExpirationHours, TimeUnit.HOURS);
        // 返回用户信息
        return LoginResult.builder()
                .accessToken(jwtTokenHeader + token)
                .refreshToken(jwtTokenHeader + refreshToken)
                .expires(jwtExpirationDate.getTime())
                .build();
    }

    /**
     * 过期时间设置
     *
     * @return
     */
    private Date getExpirationTime(int expirationTime) {
        Calendar instance = Calendar.getInstance();
        // Calendar.HOUR_OF_DAY 设置 expirationTime 为 24小时制的时间
        instance.add(Calendar.HOUR_OF_DAY, expirationTime);
        // 将增加后的日期返回
        return instance.getTime();
    }

    /**
     * 判断是否过期
     *
     * @param token
     * @return
     */
    public Boolean isExpiration(String token) {
        Claims claim = getClaim(jwtTokenHeader + token);
        // 如果解析出来是空，则不需要判断直接为过期
        if (Objects.isNull(claim)) return true;
        // 判断 Token 是否过期 （ true -> 过期 ，false -> 未过期 ）
        return claim.getExpiration().before(new Date());
    }

    /**
     * 拿取 Claims
     *
     * @param token token 信息
     * @return
     */
    private Claims getClaim(String token) {
        try {
            return Jwts.parser()
                    // 秘钥
                    .setSigningKey(JwtSecretKey)
                    // 解析
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e) {
            return null;
        }
    }

    /**
     * 获取 token 信息
     *
     * @param key        键
     * @param token      JwtToken
     * @param valueClass 值对应的类型
     * @param <T>        自定义返回类型
     * @return
     */
    public <T> T getTokenValueByKey(String token, String key, Class<T> valueClass) {
        Claims claim = getClaim(token);
        // 如果获取不到 Claims 表示 token 无效
        if (Objects.isNull(claim)) return null;
        // 从 jwt 中返回需要的数据
        return claim.get(key, valueClass);
    }

    /**
     * 解析请求头中的 Token
     *
     * @param req
     * @return
     */
    public String resolveToken(HttpServletRequest req) {
        String bearerToken = req.getHeader(JwtRequestHeaderKey);
        return cutToken(bearerToken);
    }

    /**
     * 切割 token
     *
     * @param token token
     * @return 切割后的 token
     */
    public String cutToken(String token) {
        if (token != null && token.startsWith(jwtTokenHeader)) {
            return token.substring(7);
        }
        return null;
    }

}
